package app;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import messages.ConnectRsp;
import messages.DisconnectRsp;
import messages.ParListRsp;
import messages.SingleTuneRsp;

import exceptions.EmptyBufferException;

import util.ByteList;
import util.CircularByteBuffer;
import util.Utils;

public class MsgDecoder extends TimerTask {

	private CircularByteBuffer	buffer;

	private static final int	STATUS_AWAITING_SYNCH_CHAR	= 0;
	private static final int	STATUS_AWAITING_LENGTH		= 1;
	private static final int	STATUS_AWAITING_DEST_ID		= 2;
	private static final int	STATUS_AWAITING_SRC_ID		= 3;
	private static final int	STATUS_AWAITING_MSG_ID		= 4;
	private static final int	STATUS_AWAITING_MSG_DATA	= 5;

	private int					currStatus;

	private short				length						= 0;
	private short				destID						= 0;
	private short				srcID						= 0;
	private short				msgID						= 0;
	private short				msgData						= 0;

	Timer						timer;

	public MsgDecoder( CircularByteBuffer _buffer ) {
		this.buffer = _buffer;
		this.currStatus = STATUS_AWAITING_SYNCH_CHAR;

		timer = new Timer( );
		timer.schedule( this , 0 , // initial delay
				1 * 500 ); // subsequent rate
	}

	@Override
	public void run( ) {
		synchronized ( buffer ) {
			if ( buffer.getCount( ) == 0 ) return; // nothing to read from buffer
		}
		switch ( currStatus ) {
			case STATUS_AWAITING_SYNCH_CHAR :
				if ( !decodeSynchChar( ) ) return;
				if ( !decodeLength( ) ) return;
				if ( !decodeDestID( ) ) return;
				if ( !decodeSrcID( ) ) return;
				if ( !decodeMsgID( ) ) return;
				if ( !decodeMsgData( ) ) return;
				break;
			case STATUS_AWAITING_LENGTH :
				if ( !decodeLength( ) ) return;
				if ( !decodeDestID( ) ) return;
				if ( !decodeSrcID( ) ) return;
				if ( !decodeMsgID( ) ) return;
				if ( !decodeMsgData( ) ) return;
				break;
			case STATUS_AWAITING_DEST_ID :
				if ( !decodeDestID( ) ) return;
				if ( !decodeSrcID( ) ) return;
				if ( !decodeMsgID( ) ) return;
				if ( !decodeMsgData( ) ) return;
				break;
			case STATUS_AWAITING_SRC_ID :
				if ( !decodeSrcID( ) ) return;
				if ( !decodeMsgID( ) ) return;
				if ( !decodeMsgData( ) ) return;
				break;
			case STATUS_AWAITING_MSG_ID :
				if ( !decodeMsgID( ) ) return;
				if ( !decodeMsgData( ) ) return;
				break;
			case STATUS_AWAITING_MSG_DATA :
				if ( !decodeMsgData( ) ) return;
				break;
		}
	}

	private boolean decodeSynchChar( ) {

		synchronized ( buffer ) {
			try {
				if ( buffer.peek( 0 ) == 0x1b && buffer.peek( 1 ) != 0xff )
					buffer.get( );
				else
					// not the byte we were hoping for. TODO ?????
					return false;
			} catch ( EmptyBufferException e ) {
				System.out.println( "MsgDecoder -> decodeSynchChar : " + e );
				return false;
			}
		}

		// move to next status
		currStatus = STATUS_AWAITING_LENGTH;

		return true;
	}

	private boolean decodeLength( ) {

		byte[] bts = getNBytes( 2 );
		if ( bts != null ) {
			this.length = Utils.bytesToShort( bts[1] , bts[0] ); // takes care of endianess
			// difference

			// move to next status
			currStatus = STATUS_AWAITING_DEST_ID;
			return true;
		}

		return false;

	}

	private boolean decodeDestID( ) {

		byte[] bts = getNBytes( 2 );
		if ( bts != null ) {
			this.destID = Utils.bytesToShort( bts[1] , bts[0] ); // takes care of endianess
			// difference

			// move to next status
			currStatus = STATUS_AWAITING_SRC_ID;
		}

		return false;
	}

	private boolean decodeSrcID( ) {

		byte[] bts = getNBytes( 2 );
		if ( bts != null ) {
			this.srcID = Utils.bytesToShort( bts[1] , bts[0] ); // takes care of endianess
			// difference

			// move to next status
			currStatus = STATUS_AWAITING_MSG_ID;
		}

		return false;
	}

	private boolean decodeMsgID( ) {

		byte[] bts = getNBytes( 2 );
		if ( bts != null ) {
			this.msgID = Utils.bytesToShort( bts[1] , bts[0] ); // takes care of endianess
			// difference

			// move to next status
			currStatus = STATUS_AWAITING_MSG_DATA;
		}

		return false;
	}

	private boolean decodeMsgData( ) {
		byte[] tmp;
		int remaining = 0;

		switch ( this.msgID ) {
			case Utils.CONNECT_RSP_ID :

				short window_s = 0 ,
				compat = 0 ,
				error = 0;

				tmp = getNBytes( 2 );
				if ( tmp == null ) return false;
				window_s = Utils.bytesToShort( tmp[1] , tmp[0] );

				tmp = getNBytes( 2 );
				if ( tmp == null ) return false;
				compat = Utils.bytesToShort( tmp[1] , tmp[0] );

				tmp = getNBytes( 2 );
				if ( tmp == null ) return false;
				error = Utils.bytesToShort( tmp[1] , tmp[0] );

				ConnectRsp conRsp = new ConnectRsp( destID , srcID , window_s , compat , error );
				System.out.println( conRsp );

				break;

			case Utils.DISCONNECT_RSP_ID :

				short resp;

				tmp = getNBytes( 2 );
				if ( tmp == null ) return false;
				resp = Utils.bytesToShort( tmp[1] , tmp[0] );

				DisconnectRsp disconRsp = new DisconnectRsp( destID , srcID , resp );
				System.out.println( disconRsp );

				break;
			case Utils.PAR_LIST_RSP_ID :

				byte messageCount_actual = 0 ,
				messageCount_total = 0;
				tmp = getNBytes( 2 );
				if ( tmp == null ) return false;

				remaining = ( length - 10 ) / 4; // remaining shorts to read

				short[] messageID = new short[remaining];
				short[] asciiID = new short[remaining];

				for ( int i = 0 ; i < remaining ; i++ ) {
					tmp = getNBytes( 4 );
					if ( tmp == null ) return false;

					messageID[i] = Utils.bytesToShort( tmp[1] , tmp[0] );
					asciiID[i] = Utils.bytesToShort( tmp[3] , tmp[2] );
				}

				ParListRsp parRsp = new ParListRsp( destID , srcID , messageCount_actual ,
						messageCount_total , messageID , asciiID );
				System.out.println( parRsp.toString( ) );

				break;
			case Utils.SINGLE_TUNE_RSP_ID :

				short tuneID = 0;

				tmp = getNBytes( 2 );
				if ( tmp == null ) return false;
				tuneID = Utils.bytesToShort( tmp[1] , tmp[0] );

				remaining = length - 10; // remaining bytes to read

				List<Short> msgIDs = new LinkedList<Short>(); 				//shorts
				List<Short> mpbHds = new LinkedList<Short>(); 				//shorts
				List<Byte> recLens = new LinkedList<Byte>(); 				//bytes
				List<Byte> recIDs = new LinkedList<Byte>();					//bytes
				List<List<Short>> words = new LinkedList<List<Short>>();	//shorts
				
				int numBytesRead = 0 ,i = 0;
				while ( numBytesRead < remaining ) {
					tmp = getNBytes( 2 );
					if ( tmp == null ) return false;
					numBytesRead += 2;
					
					msgIDs.add( Utils.bytesToShort( tmp[1] , tmp[0] ) );
					
					tmp = getNBytes( 2 );
					if ( tmp == null ) return false;
					numBytesRead += 2;
					
					mpbHds.add( Utils.bytesToShort( tmp[1] , tmp[0] ) );
					
					tmp = getNBytes( 2 );
					if ( tmp == null ) return false;
					numBytesRead += 2;
					
					recLens.add( tmp[1] );
					recIDs.add( tmp[0] );
					
					words.add( new LinkedList<Short>( ) );
					if( recLens.get( i ) > 0){ 	// number of words to read
						for(int j = 0 ; j < recLens.get( i ) ; j++){
							tmp = getNBytes( 2 );
							if ( tmp == null ) return false;
							numBytesRead += 2;
							words.get( i ).add( Utils.bytesToShort( tmp[1] , tmp[0] ) );
						}
					}
					
					i++;
				}

				SingleTuneRsp singleTRes = new SingleTuneRsp( destID , srcID , tuneID , convertToShortArray( msgIDs) , convertToShortArray(mpbHds) , convertToByteArray( recLens ) , convertToByteArray( recIDs ) , convertToShortListArray( words) );
				System.out.println( singleTRes );
				break;
				
			default:
					System.out.println("[MsgDecoder] Unrecognized Message. ID = "+this.msgID);
		}

		// move to next status
		currStatus = STATUS_AWAITING_SYNCH_CHAR;

		return true;
	}
	
	private List[] convertToShortListArray(List<List<Short>> list) {
		List[] array = new List[list.size( )];
		for(int i = 0;i<array.length;i++)
			array[i] = list.get( i );
		
		return array;
	}
	
	private short[] convertToShortArray(List<Short> list) {
		short[] array = new short[list.size( )];
		for(int i = 0;i<array.length;i++)
			array[i] = list.get( i );
		
		return array;
	}
	
	private byte[] convertToByteArray(List<Byte> list) {
		byte[] array = new byte[list.size( )];
		for(int i = 0;i<array.length;i++)
			array[i] = list.get( i );
		
		return array;
	}

	/**
	 * 
	 * Reads 'n' bytes from buffer. Takes care of cleaning up all escape chars present in data
	 * sequence.
	 * 
	 * @param n
	 *        number of bytes to read from buffer
	 * @return array of bytes read from buffer
	 */
	private byte[] getNBytes( int n ) {
		byte[] bts = new byte[n];
		int[] indexes = new int[n];
		int cont_bytes_read = 0 , i = 0;
		try {
			synchronized ( buffer ) {
				while ( cont_bytes_read < n ) {
					if ( buffer.peek( i ) != 0x1b ) {
						indexes[cont_bytes_read] = i;
						cont_bytes_read++;
						i++;
					} else {
						indexes[cont_bytes_read] = i;
						cont_bytes_read++;
						i += 2;
					}
				}
				byte b;
				for ( int j = 0 , s = 0 ; j < i ; j++ ) {
					b = buffer.get( );
					if ( Arrays.binarySearch( indexes , j ) > 0 ) {
						bts[s] = b;
						s++;
					}
				}
			}
		} catch ( Exception e ) {
			return null;
		}
		return bts;
	}

}
